1 Introducción

En el presente informe se muestra el trabajo realizado para el curso “Introducción a la Minería de Datos”" abordando el dataset a utilizar con su respectiva exploración, ideas e hipótesis propuestas a partir de este, trabajo desarrollado y sus respectivos resultados.

La base de datos utilizada tiene como tema “reseñas de libros de amazon”, las cuales presentan la opinión de usuarios y lectores acerca de una gran cantidad de libros con una evaluación hacia estos en un sistema de estrellas del uno al cinco. Además cada reseña escrita puede ser evaluada por otros usuarios como positiva o no para tener una idea de si la opinión es compartida por más gente.

El objetivo del proyecto es hacer uso de técnicas de minería de datos aprendidas en el curso que nos permitan entender y desarrollar la base de datos según necesidad. Para esto, se presentan tres hipótesis fundamentadas en base al análisis exploratorio: Se puede predecir el ranking de un producto en base al texto de la reseña; Se puede clasificar si una reseña es útil o no en base al texto de una reseña; se puede crear un sistema de recomendación de libros en base a agrupaciones de usuarios. Finalmente se concluirá si las hipotesis propuestas son realizables a partir de resultados.

2 Descripción de datos

La base de datos posee una gran catidad de muestras e información sobre reseñas de libros hechas por los mismos usuarios llegando a aproximadamente 3.2Gb de datos. Esta viene escrita en forma de matriz, donde cada fila representa a una reseña y cada columna a un atributo de esta. Entrando en detalle, cada columna corresponde a:

Respecto a el dataset, se obtiene la siguiente información inicial:

#Número de filas (cantidad de reseñas)
nrow(bd)
## [1] 3105238
#Número de clientes
nrow(data.frame(unique(bd$customer_id)))
## [1] 1502291
#Número de productos
nrow(data.frame(unique(bd$product_parent)))
## [1] 666010

Se observa que la cantidad de reseñas es mayor al número de clientes y de productos, por lo que hay clientes que realizan más de una reseña y productos a los que se les hace más de una reseña.

#Espacio de tiempo entre datos
max(bd$review_date)-min(bd$review_date)
## Time difference of 3765 days

Los datos abarcan un periodo de aproximadamente 10 años.

#Última fecha:
max(bd$review_date)
## [1] "2005-10-14"
#Primera fecha:
min(bd$review_date)
## [1] "1995-06-24"

3 Análisis Exploratorio

Con respecto a las reseñas:

summary(bd$star_rating)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   1.000   4.000   5.000   4.183   5.000   5.000
acumulado <- data.frame(table(bd$star_rating))
ggplot(acumulado) +  
  geom_bar(aes(x = Var1, y = Freq), stat="identity") +   
  ggtitle("Ranking de Reseñas") + 
  xlab("Estrellas") + ylab("Frecuencia (cantidad)")  

rm(acumulado)

Se observa que más de la mitad de las reseñas son de 5 estrellas. Al disminuir la cantidad de estrellas disminuye el número de reseñas, a excepción de un alza en la cantidad reseñas de 1 estrella. Esto posiblemente se da debido a que un cliente muy insatisfecho tiene mayor probabilidad de comentar que un cliente medianamente insatisfecho (2 o 3 estrellas), por lo que para hacer recomendaciones podría ser más relevante considerar solo los reseñas de 1, 4 y 5 estrellas.

acumulado_tiempo <- format(bd$review_date, format="%Y")
acumulado_tiempo <- data.frame(table(acumulado_tiempo))
ggplot(acumulado_tiempo) + 
  geom_bar(aes(x = acumulado_tiempo, y = Freq), stat="identity") + 
  ggtitle("Cantidad de Reviews Anuales") + 
  xlab("Fecha") + ylab("Frecuencia (cantidad)")

rm(acumulado_tiempo)

Desde el inicio de Amazon la cantidad de reseñas ha aumentado bastante año a año, llegando a su peak el año 2000 y mantenido una cantidad relativamente constante en los años posteriores.

Cantidad de reseñas por cliente:

num_reviews <- data.frame(table(bd$customer_id))
num_reviews <- data.frame(id=num_reviews$Var1, Cantidad=num_reviews$Freq, Conteo=c(rep(1,nrow(num_reviews))))
review_persona <- aggregate(Conteo ~ Cantidad, num_reviews, FUN=sum)

ggplot(review_persona) +  
  geom_bar(aes(x = Cantidad, y = Conteo), stat="identity") +   
  ggtitle("Número de Reseñas por Cliente") + 
  xlab("Cantidad de Reseñas por cliente") + ylab("Cantidad de clientes")  +
  xlim(0,10)

rm(num_reviews)

Se observa que la amplia mayoría de clientes ha realizado solo 1 reseña. Para observar mejor lo que ocurre luego, se expone el mismo gráfico visualizando a partir de 3 reseñas por cliente:

ggplot(review_persona) +  
  geom_bar(aes(x = Cantidad, y = Conteo), stat="identity") +   
  ggtitle("Número de Reseñas por Cliente (a partir de 3 clientes)") + 
  xlab("Cantidad de Reseñas por cliente") + ylab("Cantidad de clientes")  +
  xlim(2,25)+
  ylim(0,2100)

A pesar de que la tendencia es rápidamente decreciente, hay una cantidad considerable de clientes que han realizado más de una reseña. Muchos clientes son clientes frecuentes y tienen el hábito de realizar reseñas al comprar un producto, sin embargo hay algunos clientes que tienen una cantidad de reseñas no creíbles. El máximo de reseñas de parte de un cliente es de 1048 lo que podría significar que hay reseñas falsas, y que se deben tratar como outliers.

#máxima cantidad de reviews por una persona
max(review_persona$Cantidad)
## [1] 21922
rm(review_persona)

3.1 Palabras más utilizadas.

Como primer acercamiento al uso del dataset se tiene como idea utilizar el texto mismo de la reseña, o incluso su título, para obtener más información a partir de este. Teniendo en cuenta los atributos presentes en cada muestra, se toman como hipótesis: conocer la evaluación en estrellas (de uno al cinco) que le da la reseña al libro a partir unicamente del texto o del título; clasificar si una reseña es útil o no en base al texto de una reseña.

Para esto, se visualizan las palabras más usadas en las mejores (5 estrellas) y peores (1 y 2 estrellas) reseñas respectivamente:

#Mejores Reviews
jeopCorpus <- bd[bd$star_rating==5, ]
jeopCorpus <- jeopCorpus[sample(nrow(jeopCorpus), as.integer(nrow(jeopCorpus)*0.1)), ] 
jeopCorpus <- jeopCorpus$review_body
jeopCorpus <- Corpus(VectorSource(jeopCorpus))
jeopCorpus <- tm_map(jeopCorpus, content_transformer(tolower))
jeopCorpus <- tm_map(jeopCorpus, removePunctuation)
jeopCorpus <- tm_map(jeopCorpus, removeWords, stopwords("english"))
jeopCorpus <- tm_map(jeopCorpus, stemDocument)
wordcloud(jeopCorpus, type=c("text", "url", "file"), 
          lang="english", excludeWords = FALSE, 
          textStemming = FALSE,  colorPalette="Dark2",
          max.words=50)

#Peores Reviews
jeopCorpus <- bd[bd$star_rating<=2, ]
jeopCorpus <- jeopCorpus[sample(nrow(jeopCorpus), as.integer(nrow(jeopCorpus)*0.5)), ] 
jeopCorpus <- jeopCorpus$review_body
jeopCorpus <- VCorpus(VectorSource(jeopCorpus))
jeopCorpus <- tm_map(jeopCorpus, content_transformer(tolower))
jeopCorpus <- tm_map(jeopCorpus, removePunctuation)
jeopCorpus <- tm_map(jeopCorpus, removeWords, stopwords("english"))
jeopCorpus <- tm_map(jeopCorpus, stemDocument)
wordcloud(jeopCorpus, type=c("text", "url", "file"),  
          lang="english", excludeWords = FALSE, 
          textStemming = FALSE,  colorPalette="Dark2",
          max.words=50)

rm(jeopCorpus)

Se puede notar que aún eliminando las palabras más comunes del inglés, existen muchas que se repiten en ambos casos (como “book”) las cuales generan ruido y se deben agregar como stopwords. De todas maneras, se ve una tendencia asociada en cada caso donde para las reseñas positivas se mencionan palabras fuertemente positivas como “love” y “recomended”, mientras que en el caso negativo, si bien también hay palabras positivas, estas tienen un menor grado de expresividad como “good” que puede darse porque la frase era “not good”. Así mismo, estas últimas presentan palabras que cuentan como negaciones como “dont” y “just”.

Por supuesto, al momento de llevar a cabo el trabajo sobre las dos hipótesis planteadas es necesario eliminar las palabras de alta frecuencia mostradas para disminuir el ruido y utilizar algún metodo que extraiga la información de estas tal que se puedan entrenar modelos con ello.

3.2 Agrupaciones de usuarios

Una tercera idea planteada para la base de datos es crear un sistema de recomendaciones personalizada a un usuario basándose en las reseñas que ha efectuado previamente y en las que otras pesonas hicieron reseñas parecidas al mismo libro.

Inicialmente se desea conectar los libros de alguna forma, por lo cual se busca establecer relaciones entre estos. Para ello se toman en cuenta solo los autores de las reseñas que califican como positivo con 4 o 5 estrellas y hallan escrito opiniones sobre dos o más libros.

#Se eliminan elementos que tienen entre 1-3 estrellas, y se aglomera en cada usuario una lista de todos los libros que han realizado reseñas.

conect_cloud <- bd[sample(nrow(bd), as.integer(nrow(bd)*0.005)), ] 

conect_cloud$books <- conect_cloud$product_parent
conect_cloud$books[conect_cloud$star_rating <= 3] <- NA

number_off_links <- setNames(aggregate(conect_cloud$books~conect_cloud$customer_id, conect_cloud, FUN=paste, collapse = ","), list("customer_id", "books"))

Luego se descartan los usuarios, y se genera una lista de listas de libros relacionados.

#Se descartan los usuarios, y se genera una lista de listas de libros relacionados.

myList <- list()
for(i in 1:length(number_off_links$books)){
  temp_val <- strsplit(number_off_links$books[i], ",")
  if(length(temp_val[[1]]) > 1){
    myList <- append(myList, list(temp_val[[1]]))
  }
}
#Se analizan los elementos y se eliminan los libros repetidos para crear la matriz.

count_link_list <- list()
for(elm in myList){
  for(book in elm){
    count_link_list <- append(count_link_list, book)
  }
}

count_link_list <- unique(count_link_list)
#Se crea la matriz y se asigna un peso si dos elementos están relacionados. No se considera la relación consigo mismo

links <- matrix(0 , length(count_link_list), length(count_link_list))

dimnames(links) <- list(c(count_link_list), c(count_link_list))

for(link in myList){
  for(a in link){
    for(b in link){
      links[a,b] = links[a,b] + 1
      links[b,a] = links[b,a] + 1
    }
  }
  for(a in link){
    for(b in link){
      links[a,a] = 0
    }
  }
}

En esta parte se eliminan los valores de la matriz que tengan un peso menor a 3, luego se eliminan los elementos nulos de la nueva matrz.

#En esta parte se eliminan los valores de la matriz que tengan un peso menor a 3, luego se eliminan los elementos nulos de la nueva matriz.

for(a in 1:length(count_link_list)){
  for(b in 1:length(count_link_list)){
    if(links[a,b] < 2){
      links[a,b] = 0
    }
    else{
      links[a,b] = 1
    }
  }
}

delete_columns = list()
delete_row = list()

for(a in 1:length(count_link_list)){
  temp_a <- TRUE
  temp_b <- TRUE
  for(b in 1:length(count_link_list)){
    if(links[a,b] == 1){
      temp_a <- FALSE
    }  
    if(links[b,a] == 1){
      temp_b <- FALSE
    }  
  }
  if(temp_a){
    delete_columns = append(a, delete_columns)
  }
  if(temp_b){
    delete_row = append(a, delete_row)
  }
}
delete_columns<-unlist(delete_columns, use.names=FALSE)
delete_row<-unlist(delete_row, use.names=FALSE)
links<-links[,-delete_columns]
links<-links[-delete_row,]

Con esta matriz se genera un grafico de las relaciones entre los distintos libros.

#Finalmente, esta matriz se genera un grafico de las relaciones entre los distintos libros.

# convert relation matrix to graph
g <- new("graphAM", adjMat=links)

# ellipse graph with initials
attrs <- list(node=list(shape="ellipse", fixedsize=FALSE))
nAttrs <- list(label=c(count_link_list))
names(nAttrs$label) <- nodes(g)
plot(g, "neato", attrs=attrs, nodeAttrs=nAttrs)

En base a la información recopilada, y según se muestra en la figura anterior, se cree que es posible hacer recomendaciones de libros personalizadas a los usuarios a través de agrupaciones de estos según sus gustos, basados en las reseñas, las estrellas asignadas y los libros leídos.

In [1]:
#cargar el data frame:
import pandas as pd
import numpy as np
import string
from time import time
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline

df = pd.read_csv('bd2.csv')
df = df.iloc[:,[1,2]]
#df = pd.read_csv('bd_sample.csv')
#df = df.iloc[:,[7,12]]

df.head()
Out[1]:
star_rating review_headline
0 4 this book was a great learning novel!
1 3 Fun Fluff
2 4 this isn't a review
3 5 fine author on her A-game
4 4 Execellent cursor examination

Experimento 1: Predicción de estrellas

Primero se intenta predecir las estrellas a través de el método bag of words respecto a los review_headlines. Se utiliza TF-IDF para asignar una matriz con puntajes donde cada fila es una palabra y cada columna es una reseña, el propósito de esto es tener una medida numérica que exprese que tan relevante es una palabra para un documento en una colección, asignando un peso a cada palabra. También se utiliza count vectorizer que crea la misma matriz pero solo usando un conteo de cada palabra por reseña.

Primero se prepara el texto para poder procesar los datos y previo a realizar stemming se usa el método train y test. Train y test consiste en separar los datos en un conjunto de entrenamiento (usando el 70% de los datos) y de testeo (30% de los datos), se obtiene el diccionario de datos del conjunto train (aquí se aplica stemming) para entrenar el modelo.

In [2]:
df.dropna(inplace=True)
#Convertir el texto en minúsculas:
df.shape
df.loc[:, 'review_headline'] = df['review_headline'].str.lower()

#Función para remover puntuación
def remove_punctuation(text):
    return text.translate(str.maketrans('','','"'))

df['review_headline']= df['review_headline'].apply( lambda x: remove_punctuation(x))
In [3]:
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer


#Train y test:

#asignamos un random state para que los valores no cambien en futuras ejecuciones
RAN_STATE = 0

#separamos el conjunto de train y test, donde review_headline es X y star_rating es y
X_train, X_test, y_train, y_test = train_test_split(df['review_headline'], df['star_rating'], random_state = 0)

#Stemming
import nltk
from nltk.stem.snowball import SnowballStemmer

stemmer = SnowballStemmer("english")

#definimos nuestra función de tokenización
def tokens(x):
    x = x.split()
    stems = []
    [stems.append(stemmer.stem(word)) for word in x]
    return stems

Se Procede a usar countVectorizer y TfidfVectorizer. En el caso de countVectorizer se utiliza min_df = 5, lo que significa que las palabras usadas menos de 5 veces no se consideran, y en el caso de TfidfVectorizer se usa min_df = 0.0001, es decir palabras con puntaje menor que eso se eliminan. En la presentación se uso un valor de 0.01 lo que entrega valores muy malos al estar usando solo el review headline, a diferencia de nuestra otra hipotesis donde con 0.01 basta pero usando el cuerpo del review, esto se da ya que al usar solo el headline un min_df de 0.01 nos deja con un vocabulario muy reducido.

En ambos casos se utiliza N gramas de (1,2) es decir combinaciones de 1 y 2 palabras, lo que se cree que puede dar resultados mejores dado que en una reseña puede decir "good", pero también puede decir "not good" haciendo que un termino positivo al juntarlo con otra palabra tenga un sentimiento negativo.

In [4]:
vect = CountVectorizer(tokenizer=tokens, min_df = 5, ngram_range = (1,2))
X_train_vectorized = vect.fit_transform(X_train)


vectorizer = TfidfVectorizer(tokenizer = tokens, stop_words = 'english', ngram_range=(1, 2), min_df = 0.0001)
features = vectorizer.fit_transform(X_train)

Se procede a definir la función clf_test_roc_score donde se definen las métricas con las cuales se evaluará tanto regresiones como para clasificadores.

In [5]:
from sklearn.metrics import classification_report, r2_score, mean_squared_error, max_error
    
def clf_test_roc_score(clf, Type, X_train, y_train, X_test, y_test):
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(type(clf).__name__)
    
    if Type == "Classification":
      class_report = classification_report(y_test, y_pred)
      print(F"Classification Report: {class_report}.\n")
    elif Type == "Regression":
      max_err = max_error(y_test, y_pred)
      mean_sq_err = mean_squared_error(y_test, y_pred)
      r2_sc = r2_score(y_test, y_pred) 
      print(F"Regression Report: \n Max Error: {max_err} \n Mean Squared Error: {mean_sq_err} \n R2: {r2_sc}. \n")
    else:
      raise NotImplmentedError

Se escoge los modelos a evaluar, en esta seccion se esgogío: LinearRegression, AdaBoostClassifier, gaussianNB, MultinomialNB y finalmente DummyRegressor y DummyClassifier con los cuales se comparan los distintos modelos. Se espera que el desempeño de los modelos al menos supere a dummy en evaluaciones como R2 y F2.

Luego utilizando el diccionario aprendido de los conjuntos de Train, se traduce el conjunto de Test para posterior evaluación, aquí se toma en cuenta que solo se traduce con los valores aprendidos en el entrenamiento, pero se ignoran si aparecen palabras nuevas en el conjunto de Test.

In [6]:
# Import the supervised learning models from sklearn
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import AdaBoostClassifier
from sklearn.linear_model import LinearRegression
from sklearn.dummy import DummyRegressor, DummyClassifier
from sklearn.naive_bayes import MultinomialNB

# Initialize the models using a random state were applicable.
clf_list = [
            ("Regression", LinearRegression()),
            ("Regression", DummyRegressor(strategy='mean')),
            ("Classification", DummyClassifier(strategy='most_frequent', random_state=0)),
            ("Classification", MultinomialNB()), 
            ("Classification", AdaBoostClassifier(random_state = RAN_STATE)),
           ]
x_tr = X_train_vectorized
x_te = vect.transform(X_test)

x_tr_tfidf = features
x_te_tfidf = vectorizer.transform(X_test)

Finalmente se observan los resultados de evaluar los distinto métodos, diferenciando entre Count Vectorized que es un método de tokenización de texto basado en bag of words, y Tfidf que está basado en la relevancia de las palabras para un conjunto de distintos textos.

Aquí se puede apreciar que a pesar que el Max Error en el caso de regression Dummy es superior a Lineal, la métrica R2 nos muestra una distribución de valores bastante más previsible. Se observa que en este caso Count Vectorized funciona de mejor manera.

Respecto a la clasificación, todos los modelos comparados con Dummy se observan con una mayor precisión por poco, lo que nos dice que el modelo con este método es poco previsible aunque la métrica f1 para multinomial Naive Bayes es la mejor evaluada también utilizando Count Vectorized.

In [7]:
# Visualize Regretions and Classifiers.                                                               
for (Type, clf) in clf_list:
  print("Count Vectorized:")
  clf_test_roc_score(clf, Type, x_tr, y_train, x_te, y_test)
  print("Tfidf:")
  clf_test_roc_score(clf, Type, x_tr_tfidf, y_train, x_te_tfidf, y_test)
Count Vectorized:
LinearRegression
Regression Report: 
 Max Error: 6.760033009919322 
 Mean Squared Error: 0.8607061945985416 
 R2: 0.4445316666166893. 

Tfidf:
LinearRegression
Regression Report: 
 Max Error: 4.874546599649404 
 Mean Squared Error: 1.0500784477269591 
 R2: 0.32231773287900034. 

Count Vectorized:
DummyRegressor
Regression Report: 
 Max Error: 3.1820915453647647 
 Mean Squared Error: 1.549520748731214 
 R2: -4.081813361755948e-06. 

Tfidf:
DummyRegressor
Regression Report: 
 Max Error: 3.1820915453647647 
 Mean Squared Error: 1.549520748731214 
 R2: -4.081813361755948e-06. 

Count Vectorized:
DummyClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.00      0.00      0.00     59356
           2       0.00      0.00      0.00     41496
           3       0.00      0.00      0.00     62393
           4       0.00      0.00      0.00    146292
           5       0.60      1.00      0.75    466763

    accuracy                           0.60    776300
   macro avg       0.12      0.20      0.15    776300
weighted avg       0.36      0.60      0.45    776300
.

Tfidf:
DummyClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.00      0.00      0.00     59356
           2       0.00      0.00      0.00     41496
           3       0.00      0.00      0.00     62393
           4       0.00      0.00      0.00    146292
           5       0.60      1.00      0.75    466763

    accuracy                           0.60    776300
   macro avg       0.12      0.20      0.15    776300
weighted avg       0.36      0.60      0.45    776300
.

Count Vectorized:
MultinomialNB
Classification Report:               precision    recall  f1-score   support

           1       0.45      0.54      0.49     59356
           2       0.32      0.23      0.27     41496
           3       0.37      0.35      0.36     62393
           4       0.38      0.28      0.32    146292
           5       0.76      0.83      0.79    466763

    accuracy                           0.63    776300
   macro avg       0.46      0.45      0.45    776300
weighted avg       0.61      0.63      0.62    776300
.

Tfidf:
MultinomialNB
Classification Report:               precision    recall  f1-score   support

           1       0.57      0.29      0.38     59356
           2       0.42      0.09      0.15     41496
           3       0.45      0.15      0.22     62393
           4       0.45      0.10      0.17    146292
           5       0.66      0.97      0.79    466763

    accuracy                           0.64    776300
   macro avg       0.51      0.32      0.34    776300
weighted avg       0.58      0.64      0.56    776300
.

Count Vectorized:
AdaBoostClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.64      0.15      0.25     59356
           2       0.39      0.08      0.13     41496
           3       0.43      0.09      0.15     62393
           4       0.41      0.14      0.21    146292
           5       0.65      0.96      0.78    466763

    accuracy                           0.63    776300
   macro avg       0.50      0.28      0.30    776300
weighted avg       0.57      0.63      0.54    776300
.

Tfidf:
AdaBoostClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.62      0.14      0.23     59356
           2       0.40      0.06      0.11     41496
           3       0.44      0.06      0.11     62393
           4       0.42      0.09      0.15    146292
           5       0.64      0.98      0.77    466763

    accuracy                           0.62    776300
   macro avg       0.50      0.27      0.27    776300
weighted avg       0.57      0.62      0.52    776300
.

Caso Subsampling

Como se observa en el análisis exploratorio el número de reseñas de 5 estrellas es considerablemente mayor a las otras estrellas por lo que al estar sobrerrepresentadas es posible que nuestras predicciones están simplemente sobre prediciendo 5 estrellas, es por esto que se hace exactamente el mismo código pero con subsampling en el conjunto de Test.

In [8]:
#Acá se hace el subsampling
from imblearn.under_sampling import RandomUnderSampler
import matplotlib.pyplot as plt
X_train2 = np.array(X_train).reshape(-1, 1)
rus = RandomUnderSampler(random_state=0)
X_resampled, y_resampled = rus.fit_resample(X_train2, y_train)


vect_sub = CountVectorizer(tokenizer=tokens, min_df = 5, ngram_range = (1,2))
X_train_vectorized_sub = vect_sub.fit_transform(X_resampled.ravel())

vectorizer_sub = TfidfVectorizer(tokenizer = tokens, stop_words = 'english', ngram_range=(1, 2), min_df = 0.0001)
features_sub = vectorizer_sub.fit_transform(X_resampled.ravel())
/anaconda3/lib/python3.7/site-packages/sklearn/externals/six.py:31: DeprecationWarning: The module is deprecated in version 0.21 and will be removed in version 0.23 since we've dropped support for Python 2.7. Please rely on the official version of six (https://pypi.org/project/six/).
  "(https://pypi.org/project/six/).", DeprecationWarning)

Aquí podemos observar una reducción general de la predictibilidad de los conjuntos de pruebas, entrenando con un conjunto balanceado, esto nos sugiere que parte de la anterior predicción se debía a que se trataba de considerar la mayoría como métrica. Se observa que la regresión nos entrega un resultado bastante malo el cual si está alejada de la predicción de dummy pero lejos de algo relevante. Por otra parte si usamos clasificación, encontramos que los valores obtenidos por Multinomial Naive Bayes muestran valores interesantes donde a pesar de que se entrenó con un conjunto balanceado, a la hora de probar con un conjunto real el modelo si aprendió del sistema, esto también utilizando Count Vectorized.

In [9]:
x_tr_sub = X_train_vectorized_sub
x_te_sub = vect_sub.transform(X_test)

x_tr_tfidf_sub = features_sub
x_te_tfidf_sub = vectorizer_sub.transform(X_test)


for (Type, clf) in clf_list:
  print("Count Vectorized:")
  clf_test_roc_score(clf, Type, x_tr_sub, y_resampled, x_te_sub, y_test)
  print("Tfidf:")
  clf_test_roc_score(clf, Type, x_tr_tfidf_sub, y_resampled, x_te_tfidf_sub, y_test)
Count Vectorized:
LinearRegression
Regression Report: 
 Max Error: 6.811759796147973 
 Mean Squared Error: 1.457624550703376 
 R2: 0.05930236710396586. 

Tfidf:
LinearRegression
Regression Report: 
 Max Error: 4.6357710252664095 
 Mean Squared Error: 1.7824727907064066 
 R2: -0.15034281915050163. 

Count Vectorized:
DummyRegressor
Regression Report: 
 Max Error: 2.0 
 Mean Squared Error: 2.952806904547211 
 R2: -0.9056336998208834. 

Tfidf:
DummyRegressor
Regression Report: 
 Max Error: 2.0 
 Mean Squared Error: 2.952806904547211 
 R2: -0.9056336998208834. 

Count Vectorized:
DummyClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.08      1.00      0.14     59356
           2       0.00      0.00      0.00     41496
           3       0.00      0.00      0.00     62393
           4       0.00      0.00      0.00    146292
           5       0.00      0.00      0.00    466763

    accuracy                           0.08    776300
   macro avg       0.02      0.20      0.03    776300
weighted avg       0.01      0.08      0.01    776300
.

Tfidf:
DummyClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.08      1.00      0.14     59356
           2       0.00      0.00      0.00     41496
           3       0.00      0.00      0.00     62393
           4       0.00      0.00      0.00    146292
           5       0.00      0.00      0.00    466763

    accuracy                           0.08    776300
   macro avg       0.02      0.20      0.03    776300
weighted avg       0.01      0.08      0.01    776300
.

Count Vectorized:
MultinomialNB
Classification Report:               precision    recall  f1-score   support

           1       0.32      0.57      0.41     59356
           2       0.23      0.39      0.29     41496
           3       0.30      0.37      0.33     62393
           4       0.31      0.40      0.35    146292
           5       0.83      0.60      0.70    466763

    accuracy                           0.53    776300
   macro avg       0.40      0.46      0.42    776300
weighted avg       0.62      0.53      0.56    776300
.

Tfidf:
MultinomialNB
Classification Report:               precision    recall  f1-score   support

           1       0.23      0.54      0.33     59356
           2       0.19      0.33      0.24     41496
           3       0.24      0.30      0.27     62393
           4       0.30      0.35      0.32    146292
           5       0.80      0.54      0.65    466763

    accuracy                           0.48    776300
   macro avg       0.35      0.41      0.36    776300
weighted avg       0.59      0.48      0.51    776300
.

Count Vectorized:
AdaBoostClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.12      0.64      0.20     59356
           2       0.26      0.18      0.21     41496
           3       0.36      0.20      0.26     62393
           4       0.28      0.24      0.25    146292
           5       0.75      0.44      0.56    466763

    accuracy                           0.38    776300
   macro avg       0.35      0.34      0.30    776300
weighted avg       0.56      0.38      0.43    776300
.

Tfidf:
AdaBoostClassifier
Classification Report:               precision    recall  f1-score   support

           1       0.51      0.17      0.26     59356
           2       0.07      0.74      0.12     41496
           3       0.25      0.15      0.19     62393
           4       0.40      0.08      0.13    146292
           5       0.77      0.36      0.49    466763

    accuracy                           0.30    776300
   macro avg       0.40      0.30      0.24    776300
weighted avg       0.60      0.30      0.36    776300
.

Experimento 2: Utilidad de una reseña en base a su texto

Después de que una reseña es publicada recibe votos de otros usuarios que luego de leerla, la evaluan positiva o negativamente según si esta les pareció o no útil. Se plantea que es posible determinar si una reseña será considerada útil o no en base a su texto, de esta forma se podría saber la calidad de la reseña incluso antes de ser publicada. Para este experimento serán necesarias las columnas "review_body", que contienen el cuerpo de la reseña "total_votes", el total de votos que se le hizo, y "helpful_votes", la cantidad de votos positivos que tuvo.

In [1]:
#cargar el data frame:
import pandas as pd
import numpy as np
import string
from time import time
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline


db = pd.read_csv('bd_original.csv')
df = db.iloc[:, [13,12,9,8,7]]
df.head() 
Out[1]:
review_date review_body vine total_votes helpful_votes
0 2005-10-14 this boook was a great one that you could lear... N 3 2
1 2005-10-14 If you are looking for something to stimulate ... N 5 5
2 2005-10-14 never read it-a young relative idicated he lik... N 22 1
3 2005-10-14 Though she is honored to be Chicago Woman of t... N 2 2
4 2005-10-14 Review based on a cursory examination by Unive... N 2 0

Los datos que hayan tenido menos de 10 clasificaciones se eliminarán primero del conjunto de datos. Como muchas de estas reseñas pueden ser buenas, pero por casualidad, no se lean ni se califiquen, nuestro algoritmo las evaluará negativamente y hará que nuestro modelo clasifique incorrectamente futuras "buenas" reseñas.

In [2]:
#incluir solo reviews con mas de 10 votos totales
df1 = df[(df.total_votes > 10)].copy()
df1.shape
Out[2]:
(960425, 5)

Deseamos determinar si un determinado texto de revisión es útil o no, para eso necesitamos una forma de asignar los datos existentes a esta clasificación binaria. El método elegido es utilizar un umbral de 'helpful_votes' dividido por 'total_votes', es decir, la proporción de personas que encontraron la revisión útil sobre el total de las personas que la calificaron. Si esta relación excede un cierto valor de umbral, podemos etiquetar los datos de entrenamiento útiles como'helpful' = 1, y los no útiles como 'helpful' = 0. Para este análisis, el umbral se establece en 0.85, ya que de esta forma se obtiene aproximadamente la mitad de las reseñas marcadas como útiles y la otra mitad como no útiles.

In [3]:
#umbral
threshold = 0.85

# se divide votos buenos por total y de revisa si es mayor al umbral
df1.loc[:, 'Helpful'] = np.where(df1.loc[:, 'helpful_votes'] \
                                 / df1.loc[:, 'total_votes'] > threshold, 1, 0)

#debería imprimir ~0.5 si lla mitad es 1
print(df1['Helpful'].mean())

df1.head(10)
0.5004336621808054
Out[3]:
review_date review_body vine total_votes helpful_votes Helpful
2 2005-10-14 never read it-a young relative idicated he lik... N 22 1 0
6 2005-10-14 This book is chilling and depressing indeed, t... N 11 9 0
8 2005-10-14 Never been much for enjoying history, but the ... N 20 16 0
12 2005-10-14 Whether intentional or not, this book's audien... N 14 14 1
14 2005-10-14 At last... a comprehensive visual compendium o... N 19 16 0
15 2005-10-14 I find \\"Photoshop  for Nature Photographers... N 14 13 1
16 2005-10-14 I love Aristotle and this is a good collection... N 35 17 0
17 2005-10-14 To his discredit, Dr. Unwin virtually brags in... N 34 16 0
22 2005-10-14 This book isn't merely a primer for capitalist... N 31 21 0
23 2005-10-14 The Outlander series are my favorite books of ... N 12 6 0

Se preprocesa el texto de las reseñas para luego utilizar el vectorizador TF-IDF de la librería sci-kit learn y generar los features de las reseñas.

In [4]:
import os
from sklearn.feature_extraction.text import TfidfVectorizer
import nltk
from nltk.stem.snowball import SnowballStemmer

# convertir el texto a minúscula
df1.loc[:, 'review_body'] = df1['review_body'].str.lower()

#método para quitar la puntuaciónn
def remove_punctuation(text):
    return text.translate(str.maketrans('','','"'))

#se aplica remove_punctuation
df1['review_body']=df1['review_body'].apply( lambda x: remove_punctuation(x))
#df1['review_body'].head(4)


#crear un stemmer
stemmer = SnowballStemmer("english")


#define our own tokenizing function that we will pass into the TFIDFVectorizer. We will also stem the words here.
#se define una función tokenizing que se le pasará a TFIDFVectorizer, también se aplica el stemming
def tokens(x):
    x = x.split()
    stems = []
    [stems.append(stemmer.stem(word)) for word in x]
    return stems

#se define el vectorizer
vectorizer = TfidfVectorizer(tokenizer = tokens, stop_words = 'english', ngram_range=(1, 1), min_df = 0.01)
#se le aplica el vectorizador a los datos
features = vectorizer.fit_transform(df1['review_body'])
features
Out[4]:
<960425x1313 sparse matrix of type '<class 'numpy.float64'>'
	with 41932333 stored elements in Compressed Sparse Row format>

Se define el método "clf_crossVal" que toma un clasificador y entrena con este usando cross validation, luego imprime el promedio de los resultados.

In [5]:
from sklearn.metrics import roc_auc_score, roc_curve, accuracy_score, classification_report
from sklearn.model_selection import cross_validate
    

def clf_crossVal(clf, X, y ):
       
    scoring = ['precision_macro', 'recall_macro', 'accuracy', 'f1_macro', 'roc_auc']
    cv_results = cross_validate(clf, X, y, cv = 7, scoring = scoring, return_train_score= True)
    
    print(type(clf).__name__)
    
    print('Promedio Precision:', np.mean(cv_results['test_precision_macro']))
    print('Promedio Recall:', np.mean(cv_results['test_recall_macro']))
    print('Promedio F1-score:', np.mean(cv_results['test_f1_macro']))
    print('Promedio Accucary:', np.mean(cv_results['test_accuracy']))
    print('Promedio ROC_AUC:', np.mean(cv_results['test_roc_auc']))
    print("\n")

Se define una lista de los clasificadores a entrenar. El primero, DummyClassifier con el parametro "stratified" genera predicciones respetando la distribución de clases del conjunto de entrenamiento. El segundo multinomial naive bayes que se suele ocupar para analisis de texto. Luego AdaBoostClassifier, inicialmente también se quería entrenar con RandomForest y Decision, pero dado que su tiempo de entrenamiento era muy largo, y para texto da malos rasultados, se optó por probar solo con un método basado en árbol de decisión. Finalmente se entrena con regresión logística

In [6]:
# Import the supervised learning models from sklearn
from sklearn.naive_bayes import MultinomialNB
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.dummy import DummyRegressor
from sklearn.dummy import DummyClassifier



# Initialize the models.
clf_list = [DummyClassifier(strategy='stratified'),
            MultinomialNB(), 
            AdaBoostClassifier(),
            LogisticRegression()]

Para cada clasificador en la lista se usa el método antes definido X= los features generados e y= columna "helpful" definida anteriormente.

In [7]:
import matplotlib.pyplot as plt
X=features
y=df1['Helpful']

# se recorre la lista de clasificadores                                                    
for clf in clf_list:
    clf_crossVal(clf, X,y)
    
    
DummyClassifier
Promedio Precision: 0.5001698712810067
Promedio Recall: 0.4997641122927486
Promedio F1-score: 0.5002129719220431
Promedio Accucary: 0.49972563963337663
Promedio ROC_AUC: 0.5001576468612211


MultinomialNB
Promedio Precision: 0.6763691175487889
Promedio Recall: 0.6741895154673064
Promedio F1-score: 0.6731109085916362
Promedio Accucary: 0.6741984160127282
Promedio ROC_AUC: 0.7394333407851629


AdaBoostClassifier
Promedio Precision: 0.6591755973576413
Promedio Recall: 0.6575470946249764
Promedio F1-score: 0.6566163261059076
Promedio Accucary: 0.6575130803162559
Promedio ROC_AUC: 0.7151770349812464


LogisticRegression
Promedio Precision: 0.7092144388533997
Promedio Recall: 0.7080351792777924
Promedio F1-score: 0.7075992506718768
Promedio Accucary: 0.7080375972944682
Promedio ROC_AUC: 0.7781599949481189


Se puede ver de los resultados que todos los clasificadores tienen mejores promedios que los de el clasificador Dummy, pero el único que da realmente buenos resultados es la regrasión lógistica. Con lo que se concluye que es posible acercarse a determinar si una reseña será evaluada positivamente o no.